﻿/*
Copyright © 2011 François Karman

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

See <license.txt> for the complete license of the software.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Simple.Storage.Api
{
    /// <summary>
    /// The default implementation of the <see cref="IClassDefinition"/> interface.
    /// </summary>
    /// <typeparam name="T">
    /// The type of the persisted objects.
    /// </typeparam>
    /// <typeparam name="TKey">
    /// The type of the identifier associated with <typeparamref name="T"/>.
    /// </typeparam>
    public class DefaultClassDefinition<T, TKey> : IClassDefinition
    {
        /// <summary>
        /// The function used to extract the identifier of a <typeparamref name="T"/> object.
        /// </summary>
        private readonly Func<T, TKey> retrieveKey;

        /// <summary>
        /// The function used to create new instance of the <typeparamref name="T"/> class.
        /// </summary>
        private readonly Func<TKey, T> createInstance;

        /// <summary>
        /// The serialized properties.
        /// </summary>
        private readonly ICollection<IPropertyDefinition> properties;

        /// <summary>
        /// The read-only collection associated with <c>DefaultClassDefinition.properties</c>.
        /// </summary>
        private readonly ICollection<IPropertyDefinition> propertiesRO;

        /// <summary>
        /// Initializes a new instance of the DefaultClassDefinition class.
        /// </summary>
        /// <param name="retrieveKey">
        /// The function used to extract the identifier of a <typeparamref name="T"/> object.
        /// </param>
        /// <param name="createInstance">
        /// The function used to create new instance of the <typeparamref name="T"/> class.
        /// </param>
        public DefaultClassDefinition(Func<T, TKey> retrieveKey, Func<TKey, T> createInstance)
        {
            if (retrieveKey == null)
            {
                throw new ArgumentNullException("retrieveKey");
            }
            else if (createInstance == null)
            {
                throw new ArgumentNullException("createInstance");
            }

            this.properties = new LinkedList<IPropertyDefinition>();
            this.propertiesRO = new ReadOnlyCollection<IPropertyDefinition>(this.properties);

            this.retrieveKey = retrieveKey;
            this.createInstance = createInstance;

            this.Initialize();
        }

        /// <summary>
        /// Gets the type of the persisted instances.
        /// </summary>
        public Type Type
        {
            get { return typeof(T); }
        }

        /// <summary>
        /// Gets the type of the associated key.
        /// </summary>
        public Type KeyType
        {
            get { return typeof(TKey); }
        }

        /// <summary>
        /// Gets the definitions of the serialized properties.
        /// </summary>
        /// <remarks>
        /// This collection is read-only.
        /// </remarks>
        public ICollection<IPropertyDefinition> Properties
        {
            get { return this.propertiesRO; }
        }

        /// <summary>
        /// Extracts all the serializable properties of the associated type.
        /// </summary>
        public void Initialize()
        {
            foreach (PropertyInfo property in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                bool canRead = property.CanRead && property.GetGetMethod() != null;
                bool canWrite = property.CanWrite && property.GetSetMethod() != null;

                if (canRead && canWrite)
                {
                    // getter and setter available
                    PropertyInfo p = property;
                    IPropertyDefinition definition = new DefaultPropertyDefinition(
                        p.PropertyType,
                        obj => p.GetValue(obj, null),
                        (obj, value) => p.SetValue(obj, value, null));

                    this.properties.Add(definition);
                }
                else if (canRead)
                {
                    // only a getter: in-place serialization
                    bool collection = property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>);
                    Type interfaceType = property.PropertyType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>));
                    if (collection || interfaceType != null)
                    {
                        PropertyInfo p = property;
                        IPropertyDefinition definition = new DefaultPropertyDefinition(
                            p.PropertyType,
                            obj => p.GetValue(obj, null),
                            null);

                        this.properties.Add(definition);
                    }
                }
            }
        }

        /// <summary>
        /// Retrieves the unique key of an object associated with this definition.
        /// </summary>
        /// <param name="value">A <see cref="IClassDefinition.Type"/> instance.</param>
        /// <returns>
        /// The associated identifier  of <see cref="IClassDefinition.KeyType"/> type.
        /// </returns>
        public object RetrieveKey(object value)
        {
            if (value is T)
            {
                return this.retrieveKey((T)value);
            }
            else
            {
                // TODO: review the exception
                throw new NotSupportedException();
            }
        }

        /// <summary>
        /// Creates an new instance of the persisted class.
        /// </summary>
        /// <param name="key">The unique key of the instance.</param>
        /// <returns>The created instance.</returns>
        /// <remarks>
        /// The <paramref name="key"/> should only be used if the key
        /// is not part ofte serialized properties.
        /// </remarks>
        public object CreateInstance(object key)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }
            else if (!(key is TKey))
            {
                string message = string.Format(
                    Messages.Culture,
                    Messages.DataSourceWrongKeyType,
                    typeof(T).FullName,
                    typeof(TKey).FullName,
                    key.GetType().FullName,
                    key);
                throw new WrongKeyTypeException(message);
            }

            return this.createInstance((TKey)key);
        }
    }
}
